package org.hong.monkey.ui

import java.net.{InetSocketAddress, URL}
import java.util.OptionalInt
import javax.servlet.DispatcherType
import javax.servlet.http.{HttpServletRequest, HttpServlet, HttpServletResponse}

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.handler.{ErrorHandler, GzipHandler, ContextHandlerCollection}
import org.eclipse.jetty.servlet.{FilterHolder, DefaultServlet, ServletHolder, ServletContextHandler}
import org.eclipse.jetty.util.thread.QueuedThreadPool
import org.hong.monkey.util.Utils
import org.hong.monkey.{MonkeyConf, Logging}
import org.json4s.JValue
import org.json4s.jackson.JsonMethods.{pretty, render}

import scala.xml.Node

private[monkey] object JettyUtils extends Logging {

  type Responder[T] = HttpServletRequest => T

  class  ServletParams[T <% AnyRef](val responder: Responder[T],
                                     val contentType: String,
                                     val extractFn: T => String = (in: Any) => in.toString)
  {}

  implicit def jsonResponderToServlet(responder: Responder[JValue]): ServletParams[JValue] =
    new ServletParams(responder, "text/json", (in: JValue) => pretty(render(in)))

  implicit def htmlResponderToServlet(responder: Responder[Seq[Node]]): ServletParams[Seq[Node]] =
    new ServletParams(responder, "text/html", (in: Seq[Node]) => "<!DOCTYPE HTML>" + in.toString)

  implicit def textResponderToServlet(responder: Responder[String]): ServletParams[String] =
    new ServletParams(responder, "text/plain")

  def createServlet[T <% AnyRef](
    servletParams: ServletParams[T],
    conf: MonkeyConf): HttpServlet = {

    val allowFramingFrom = conf.getOption("monkey.ui.alllowFramingFrom")
    val xFrameOptionsValue =
    allowFramingFrom.map(uri => s"ALLOW-FROM $uri").getOrElse("SAMEORIGIN")

    new HttpServlet {
      override def doGet(request: HttpServletRequest, response: HttpServletResponse): Unit = {
        try {
          response.setContentType("%s;charset=utf-8".format(servletParams.contentType))
          response.setStatus(HttpServletResponse.SC_OK)
          val result = servletParams.responder(request)
          response.setHeader("Cache-Control", "no-cacahe, no-store, mast-revalidate")
          response.setHeader("X-Frame-Options", xFrameOptionsValue)
          response.getWriter.println(servletParams.extractFn(result))
        } catch {
          case e: IllegalArgumentException =>
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage())
          case e:Exception =>
            logWarning(s"Get ${request.getRequestURI} failed: $e", e)
            throw e
        }
      }

      override def doTrace(req: HttpServletRequest, res: HttpServletResponse): Unit = {
        res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
      }
    }
  }

  def createServletHandler[T <% AnyRef](
    path: String,
    servletParams: ServletParams[T],
    conf: MonkeyConf,
    basePath: String = ""): ServletContextHandler = {
    createServletHandler(path, createServlet(servletParams, conf), basePath)
  }

  def createServletHandler(
    path:String,
    servlet: HttpServlet,
    basePath: String): ServletContextHandler = {

    val prefixedPath = if (basePath == "" && path == "/") path else
      (basePath + path).stripSuffix("/")

    val contextHandler = new ServletContextHandler
    val holder = new ServletHolder(servlet)
    contextHandler.setContextPath(prefixedPath)
    contextHandler.addServlet(holder, "/")
    contextHandler
  }

  def createRedirectHandler(
    srcPath: String,
    destPath: String,
    beforeRedirect: HttpServletRequest => Unit = x => (),
    basePath: String = "",
    httpMethods: Set[String] = Set("GET")): ServletContextHandler = {

    val prefixdDestPath = basePath + destPath
    val servlet = new HttpServlet {
      override def doTrace(req: HttpServletRequest, resp: HttpServletResponse): Unit = {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
      }

      override def doGet(req: HttpServletRequest, resp: HttpServletResponse): Unit = {
        if (httpMethods.contains("GET")) {
          doRequest(req, resp)
        } else {
          resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
        }
      }

      private def doRequest(request: HttpServletRequest, response: HttpServletResponse): Unit = {
        beforeRedirect(request)
        val newUrl = new URL(new URL(request.getRequestURL.toString), prefixdDestPath).toString
        response.sendRedirect(newUrl)
      }

      override def doPost(req: HttpServletRequest, resp: HttpServletResponse): Unit = {
        if (httpMethods.contains("POST")) {
          doRequest(req, resp)
        } else {
          resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
        }
      }
    }
    createServletHandler(srcPath, servlet, basePath)
  }

  def createStaticHandler(resourceBase: String, path: String): ServletContextHandler = {
    val contextHandler = new ServletContextHandler()
    contextHandler.setInitParameter("org.eclipse.jetty.servletDefault.gzip", "false")
    val staticHandler = new DefaultServlet
    val holder = new ServletHolder(staticHandler)
    Option(Utils.getMonkeyClassLoader.getResource(resourceBase)) match {
      case Some(res) =>
        holder.setInitParameter("resourceBase", res.toString)
      case None =>
        throw new Exception("Could not find resource path for Web UI: " + resourceBase)
    }
    contextHandler.setContextPath(path)
    contextHandler.addServlet(holder, "/")
    contextHandler
  }

  def addFilters(handlers: Seq[ServletContextHandler], conf: MonkeyConf): Unit = {
    val filters: Array[String] = conf.get("monkey.ui.filters", "").split(",").map(_.trim())
    filters.foreach {
      case filter: String =>
        if (!filter.isEmpty) {
          logInfo("Adding filter: " + filter)
          val holder: FilterHolder = new FilterHolder()
          holder.setClassName(filter)
          conf.get("monkey."+filter+".params", "").split(",").map(_.trim()).toSeq.foreach {
            param: String =>
              if (!param.isEmpty) {
                val parts = param.split("=")
                if (parts.length == 2) holder.setInitParameter(parts(0), parts(1))
              }
          }

          val prefix = s"monkey.$filter.param."
          conf.getAll.filter {
            case (k, v) => k.length > prefix.length && k.startsWith(prefix)
          }.foreach {
            case (k, v) => holder.setInitParameter(k.substring(prefix.length), v)
          }

          val enumDispatcher = java.util.EnumSet.of(DispatcherType.ASYNC, DispatcherType.ERROR,
          DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST)
          handlers.foreach {_.addFilter(holder, "/*", enumDispatcher)}
        }
    }
  }

  def startJettyServer(
    hostName: String,
    port: Int,
    handlers: Seq[ServletContextHandler],
    conf: MonkeyConf,
    serverName: String = ""): ServerInfo = {
    addFilters(handlers, conf)
    val collection = new ContextHandlerCollection
    val gzipHandlers = handlers.map { h =>
      val gzipHandler = new GzipHandler
      gzipHandler.setHandler(h)
      gzipHandler
    }
    collection.setHandlers(gzipHandlers.toArray)

    def connect(curretnPort: Int): (Server, Int) = {
      val server = new Server(new InetSocketAddress(hostName, curretnPort))
      val pool = new QueuedThreadPool()
      pool.setDaemon(true)
      server.setThreadPool(pool)
      val errorHandler = new ErrorHandler()
      errorHandler.setShowMessageInTitle(true)
      server.addBean(errorHandler)
      server.setHandler(collection)
      try {
        server.start()
        (server, server.getConnectors.head.getLocalPort)
      } catch {
        case e: Exception =>
          server.stop()
          pool.stop()
          throw e
      }
    }

    val (server, boundPort) = Utils.startServiceOnPort[Server](port, connect, conf, serverName)
    ServerInfo(server, boundPort, collection)
  }

}

private[monkey] case class ServerInfo(
  server: Server,
  boundPort: Int,
  rootHandler: ContextHandlerCollection)
